package com.xgzit.xgzmall.product.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.xgzit.xgzmall.product.service.CategoryBrandRelationService;
import com.xgzit.xgzmall.product.vo.Catelog2Vo;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xgzit.common.utils.PageUtils;
import com.xgzit.common.utils.Query;

import com.xgzit.xgzmall.product.dao.CategoryDao;
import com.xgzit.xgzmall.product.entity.CategoryEntity;
import com.xgzit.xgzmall.product.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;


/**
 * @author xushuai
 */
@Slf4j
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
    @Autowired
    CategoryBrandRelationService categoryBrandRelationService;

    @Autowired
    StringRedisTemplate redisTemplate;

    @Autowired
    RedissonClient redisson;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<>()
        );

        return new PageUtils(page);
    }

    @Override
    public List<CategoryEntity> listWithTree() {
        // 1查出所有分类 继承了 ServiceImpl<CategoryDao, CategoryEntity>，里面的baseMapper就是传入的CategoryDao
        List<CategoryEntity> all = baseMapper.selectList(null); // 没有查询条件，传入null
        // 2组装成父子树形结构
        // 找出所有1级分类，他们的父分类id为0
        return all.stream()
                .filter(categoryEntity -> categoryEntity.getParentCid() == 0) // 过滤1级菜单
                .peek((menu) -> menu.setChildren(getChildren(menu, all)))  // 每一个一级菜单都设置它子菜单
                .sorted((menu1, menu2) -> (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort()))
                // 排序，就是前一个元素和后一个元素安装定义的规则进行比较。
                .collect(Collectors.toList());
    }

    @Override
    public void removeMenuByIds(List<Long> asList) {
        // todo 1、检查当前要删除的菜单是否被别的地方引用
        baseMapper.deleteBatchIds(asList);
    }

    /**
     * [2, 25, 225]
     */
    @Override
    public Long[] findCatelogPath(Long catelogId) {
        List<Long> paths = new ArrayList<>();
        List<Long> parentPath = findParentPath(catelogId, paths);
        // 逆序一下
        Collections.reverse(parentPath);
        return parentPath.toArray(new Long[0]);
    }

    /**
     * 级联更新所有关联的数据
     *
     * @param category: 分类实体
     * @CachePut  双写模式，修改了数据之后，将返回的数据又放一份到缓存中，
     * @CacheEvict 缓存失效模式, 指定那些缓存需要删除;  allEntries 设置为true时，清除分区内【即value里面写的】所有缓存
     * @Caching 组合多操作,同时进行多种缓存操作
     */
//    @CacheEvict(value = {"category", "product"}, key = "'getLevel1Categorys'")
//    @Caching(evict = {
//            @CacheEvict(value = {"category", "product"}, key = "'getLevel1Categorys'"),
//            @CacheEvict(value = {"category", "product"}, key = "'getCatalogJson'")
//    })
    @CacheEvict(value = {"category"}, allEntries = true)
    @Transactional
    @Override
    public void updateCascade(CategoryEntity category) {
        this.updateById(category);
        categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
    }

    // 每一个需要缓存的数据，要放到哪个名字的缓存，方便管理（可以多个）。【推荐按照业务类型分】
    // sync 表示 读 要不要加本地同步锁，解决击穿； 写模式springCache没有管【无锁】，但是有过期时间，对 实时性，一致性要求不是很高也足够
    // 当前方法的结果需要缓存，如果缓存中有，方法不用调用，如果没有就会调用方法，最后将方法的结果放入缓存。
    @Cacheable(value = {"category"}, key = "#root.method.name", sync = true)
    @Override
    public List<CategoryEntity> getLevel1Categorys() {
        log.info("调用了getLevel1Categorys方法！！！");
        long l = System.currentTimeMillis();
        List<CategoryEntity> categoryEntities = this.list(new LambdaQueryWrapper<CategoryEntity>()
                .eq(CategoryEntity::getParentCid, 0));
        System.out.println("时间：" + (System.currentTimeMillis() - l));
        return categoryEntities;
    }

    @Cacheable(value = {"category"}, key = "#root.methodName")
    @Override
    public Map<String, List<Catelog2Vo>> getCatalogJson() {
        log.info("真正查询数据库！");
        List<CategoryEntity> selectList = this.list(null);
        List<CategoryEntity> level1Categorys = getList(selectList, 0L);
        return level1Categorys.stream().collect(Collectors.toMap(
                k -> k.getCatId().toString(),
                v -> {
                    // 查出这个一级分类的 所有二级分类
                    List<CategoryEntity> categoryEntities = getList(selectList, v.getCatId());
                    List<Catelog2Vo> catelog2Vos = null;
                    if (!CollectionUtils.isEmpty(categoryEntities)) {
                        catelog2Vos = categoryEntities.stream().map(l2 -> {
                            Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                            // 找当前 二级分类的三级分类封装成Vo
                            List<CategoryEntity> level3Categorys = getList(selectList, l2.getCatId());
                            if (!CollectionUtils.isEmpty(level3Categorys)) {
                                List<Catelog2Vo.Catelog3Vo> catelog3Vos = level3Categorys.stream()
                                        .map(l3 -> new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName()))
                                        .collect(Collectors.toList());
                                // 封装
                                catelog2Vo.setCatalog3List(catelog3Vos);
                            }
                            return catelog2Vo;
                        }).collect(Collectors.toList());
                    }
                    return catelog2Vos;
                }));
    }

    // todo 堆外内存溢出异常  lettuce 使用netty进行网络通信，就是lettuce的bug
    // netty如果没有指定堆内存，默认使用 项目设置的Xmx的内存大小，可以通过-Dio.netty.maxDirectMemory 设置内存
    // 但是又不能只调大 这个内存数值（调大只是延缓）。 应该升级 lettuce 客户端 或者 切换使用 jedis
    public Map<String, List<Catelog2Vo>> getCatalogJson2() {
        // 1、缓存穿透： 大量并发查找一个数据库不存在的值，而我们没有将null写入缓存，导致缓存失效     解决：空值也缓存
        // 2、缓存雪崩： 大量并发查找前，缓存大面积同时过期了。 解决：设置过期时间+随机值
        // 3、缓存击穿： 大量并发查找一个缓存，而这1个缓存刚好前不久失效了。  解决：加锁，让一个人访问并添加缓存。

        // 加入缓存逻辑
        Map<String, List<Catelog2Vo>> catalogJsonFromDb = null;
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (StringUtils.isEmpty(catalogJSON)) {
            // 缓存没有
            log.info("缓存没有命中，调用查询数据库方法！");
            catalogJsonFromDb = getCatalogJsonFromDbWithRedissonLock();

        } else {
            log.info("缓存命中，直接返回！");
            catalogJsonFromDb = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
        }

        return catalogJsonFromDb;
    }

    /**
     * 缓存里面的数据如何与数据库保持一致： 缓存一致性问题
     * 1）、双写模式：  改完数据库，还要改缓存，大并发下 可能出现 写缓存时出现错误，
     * 比如最后一次修改的机器，改缓存快，而之前的某台机器改缓存慢。
     * 即可能导致 最后一次修改数据库的机器与最后一次修改缓存的机器不一致问题！ 有脏数据风险
     * 可以加锁，将 改数据库与改缓存变成原子操作！  还是会有 暂时性脏数据问题。
     * 2）、失效模式：  删除缓存数据，倚靠下一次主动查询更新缓存
     * 有三个请求，第一个改1删缓存，第二个改成2 并删缓存，但是这个机器慢一下，
     * 第三个请求读数据，因为1号请求删除了缓存，因此去数据库读的是 1，
     * 然后执行更新缓存操作：如果在2号请求提交并删除缓存之前完成，就没有问题；如果在2号更新删除之后完成更新操作，将变成脏数据！
     * <p>
     * 解决方案：
     * 1） 采用canal 订阅 binlog  这会增加系统复杂性。
     * 2）要求实时性、高一致性 不要用缓存！
     * 3） 我们采用： 缓存所有数据都有过期时间，数据过期会触发主动更新
     * 读写数据时，加上分布式读写锁。经常写的数据，不采用缓存！
     *
     * @return java.util.Map<java.lang.String, java.util.List < com.xgzit.xgzmall.product.vo.Catelog2Vo>>
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
        RLock lock = redisson.getLock("catalogJSON-lock");
        Map<String, List<Catelog2Vo>> dataFromDb = null;
        lock.lock(300, TimeUnit.SECONDS);
        log.info("redisson加锁成功，执行业务！");
        try {
            dataFromDb = getDataFromDb();
        } finally {
            log.info("redisson 解锁！");
            lock.unlock();
        }
        return dataFromDb;
    }

    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
        // 1、占分布式锁, redis占坑 同时设置过期时间(原子操作)，防止死锁。
        String uuid = UUID.randomUUID().toString();
        // 设置锁 过期300s, 基本上没有业务会执行300秒  保证不会业务没有执行完 锁就过期了。
        Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
        Map<String, List<Catelog2Vo>> dataFromDb = null;
        if (lock) {
            try {
                log.info("redis加锁成功，执行业务！");
                dataFromDb = getDataFromDb();
            } finally {
                log.info("业务执行完，不管是否有异常都要删除锁！");
                String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1]\n" +
                        "then\n" +
                        "    return redis.call(\"del\",KEYS[1])\n" +
                        "else\n" +
                        "    return 0\n" +
                        "end";
                Long sign = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Collections.singletonList("lock"), uuid);
                if (sign > 0L) {
                    log.info("删除锁成功！");
                } else {
                    log.info("删除锁失败！[ 可能这个锁已过期，锁不是自己的锁 等！]");
                }
            }
            // 业务超时，导致锁过期，删除是否会删除别人的锁？ 锁 加上uuid，每个人删自己的锁！
            // 如果获取到值之后，值0.01秒后过期被删，别的线程就会设置新值，而这里的if接收的结果确是 过期之前的，删除就会出错！
            // 删锁也要是原子操作， lua 脚本删锁！
            return dataFromDb;
        } else {
            log.info("redis加锁没有成功，等待500ms 重试！");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                log.info("休眠异常", e);
                Thread.currentThread().interrupt();
            }
            return getCatalogJsonFromDbWithRedisLock();
        }
    }

    private Map<String, List<Catelog2Vo>> getDataFromDb() {
        String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
        if (!StringUtils.isEmpty(catalogJSON)) {
            log.info("加锁，缓存命中，直接返回！");
            return JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
            });
        }
        log.info("加锁，缓存没有命中，真正查询数据库！");
        List<CategoryEntity> selectList = this.list(null);
        // 查所有1级分类
        List<CategoryEntity> level1Categorys = getList(selectList, 0L);
        // 封装数据
        Map<String, List<Catelog2Vo>> collect = level1Categorys.stream().collect(Collectors.toMap(
                k -> k.getCatId().toString(),
                v -> {
                    // 查出这个一级分类的 所有二级分类
                    List<CategoryEntity> categoryEntities = getList(selectList, v.getCatId());
                    List<Catelog2Vo> catelog2Vos = null;
                    if (!CollectionUtils.isEmpty(categoryEntities)) {
                        catelog2Vos = categoryEntities.stream().map(l2 -> {
                            Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());
                            // 找当前 二级分类的三级分类封装成Vo
                            List<CategoryEntity> level3Categorys = getList(selectList, l2.getCatId());
                            if (!CollectionUtils.isEmpty(level3Categorys)) {
                                List<Catelog2Vo.Catelog3Vo> catelog3Vos = level3Categorys.stream()
                                        .map(l3 -> new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName()))
                                        .collect(Collectors.toList());
                                // 封装
                                catelog2Vo.setCatalog3List(catelog3Vos);
                            }
                            return catelog2Vo;
                        }).collect(Collectors.toList());
                    }
                    return catelog2Vos;
                }));
        // 将对象转为json 放入缓存,  一定要查到数据马上放缓存，
        // 即释放锁之前放缓存，释放锁之后放缓存可能会出现放的过程中下一个拿到锁就已经去获取了，导致多次查询数据库
        String jsonString = JSON.toJSONString(collect);
        redisTemplate.opsForValue().set("catalogJSON", jsonString, 1, TimeUnit.DAYS);
        return collect;
    }

    /**
     * 从数据库查询并封装分类数据
     *
     * @return java.util.Map<java.lang.String, java.util.List < com.xgzit.xgzmall.product.vo.Catelog2Vo>>
     */
    public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithLocalLock() {
        // 加锁[ 只要是同一把锁，就能锁住需要这个锁的所有线程]：  this 代表当前对象，每一个对象都是单例的。
        // 1、同步代码块  2、
        // todo 本地锁 synchronized， JUC(lock) 只能锁住当前进程,  在分布式情况下，必须使用分布式锁
        /**
         * 将数据库的多次查询变成一次，保存起来。
         **/
        synchronized (this) {
            // 得到锁之后，再看看缓存中有没有，如果没有才继续查询
            return getDataFromDb();
        }
    }

    private List<CategoryEntity> getList(List<CategoryEntity> selectList, Long parentCid) {
//        return this.list(new LambdaQueryWrapper<CategoryEntity>()
//                .eq(CategoryEntity::getParentCid, v.getCatId()));
        return selectList.stream().filter(item -> item.getParentCid().equals(parentCid))
                .collect(Collectors.toList());
    }

    private List<Long> findParentPath(Long catelogId, List<Long> paths) {
        paths.add(catelogId);
        CategoryEntity byId = this.getById(catelogId);
        if (byId.getParentCid() != 0) {
            findParentPath(byId.getParentCid(), paths);
        }
        return paths;
    }

    /**
     * 递归查找所有菜单的子菜单
     *
     * @param root 当前菜单
     * @param all  所有菜单
     */
    private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
        List<CategoryEntity> children = all.stream()
                .filter((categoryEntity) -> {
                    return categoryEntity.getParentCid().equals(root.getCatId()); // 如果元素的父id和当前元素的id相同，就挑出来
                })
                .peek((menu) -> menu.setChildren(getChildren(menu, all)))// map是对每一项进行操作，操作完，再返回去就是了。
                .sorted((menu1, menu2) -> (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort()))
                .collect(Collectors.toList());
        return children;
    }
}
